﻿using RockBus.Configuration.Interfaces;
using RockBus.DataContracts;
using RockBus.Hosting.Interfaces;
using RockBus.Logging.Interfaces;
using RockBus.Routing.Interfaces;
using RockBus.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.Threading;
using System.Threading.Tasks;
using ServiceHostEntry = RockBus.Utilities.MutableTuple<bool, RockBus.DataContracts.PublisherReceivePortInfo, System.WeakReference<System.ServiceModel.ServiceHostBase>>;
using ServiceHostKey = System.Tuple<string, string>;

namespace RockBus.Hosting.Web
{
    public class DynamicServiceHostManager : IServiceHostManager
    {
        public DynamicServiceHostManager(
            ILogger logger,
            ISubscriptionConfigurationManager subscriptionConfigurationManager,
            IInternalConfiguration configuration)
        {
            this.ServiceHostDictionary = new ConcurrentDictionary<ServiceHostKey, ServiceHostEntry>();
            this.DynamicServiceHelper = new DynamicServiceHelper(logger);

            this.Logger = logger;
            this.SubscriptionConfigurationManager = subscriptionConfigurationManager;
            this.SubscriptionConfigurationManager.OnSubscriptionConfigurationChanged += SubscriptionConfigurationManager_OnSubscriptionConfigurationChanged;
            this.Configuration = configuration;
        }

        private ILogger Logger { get; set; }

        private ISubscriptionConfigurationManager SubscriptionConfigurationManager { get; set; }

        private IInternalConfiguration Configuration { get; set; }

        public void Start()
        {
            this.Logger.LogInfo(MethodBase.GetCurrentMethod(), "ServiceHostManager started.");
        }

        private ConcurrentDictionary<ServiceHostKey, ServiceHostEntry> ServiceHostDictionary { get; set; }

        private DynamicServiceHelper DynamicServiceHelper { get; set; }

        private int _guard;

        public async void UpdateServiceHosts(PublisherConfiguration publisherConfiguration)
        {
            // Sanity checks
            if (publisherConfiguration == null)
            {
                return;
            }

            // Ensure this method runs only once at a time
            if (Interlocked.CompareExchange(ref this._guard, 1, 0) == 1)
            {
                this.Logger.LogWarning(MethodBase.GetCurrentMethod(), "Another method was already running, skipping this time.");
                return;
            }

            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Updating ServiceHosts and activations.");

            try
            {
                // Unmark all ServiceHost
                foreach (var entry in this.ServiceHostDictionary.Values.ToArray())
                {
                    entry.Item1 = false;
                }

                var activeReceivePorts = new Dictionary<string, ReceivePortInfo>();
                foreach (var publisher in publisherConfiguration.EvaluationElementDictionary.Values.ToArray())
                {
                    foreach (var receivePortInfo in publisher.InfoDictionary.Values.ToArray())
                    {
                        // Ignore any receive ports for which the Endpoint or Address is null or empty
                        if ((receivePortInfo.ReceiveEndpointInfo == null) || (string.IsNullOrEmpty(receivePortInfo.ReceiveEndpointInfo.Address)))
                        {
                            continue;
                        }

                        // Find the associated ServiceHost and mark it to keep it running
                        var key = new ServiceHostKey(publisher.Id, receivePortInfo.Id);
                        ServiceHostEntry entry;
                        if (this.ServiceHostDictionary.TryGetValue(key, out entry))
                        {
                            ServiceHostBase serviceHost;
                            if (entry.Item3.TryGetTarget(out serviceHost))
                            {
                                // Mark the entry if the weak reference still holds
                                entry.Item1 = true;
                            }
                        }

                        // And add it to the active receive ports
                        if (!activeReceivePorts.ContainsKey(receivePortInfo.ReceiveEndpointInfo.Address))
                        {
                            activeReceivePorts.Add(receivePortInfo.ReceiveEndpointInfo.Address, receivePortInfo);
                        }
                    }
                }

                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Number of active ServiceHosts: {0}", activeReceivePorts.Count);

                // Merge the ServiceActivations with the active receive ports
                //  IIS/WAS will be responsible for creating/activating the ServiceHost
                //  We register the service host factory at the ReceivePort Address, so IIS/WAS can activate it
                //  We just need to make sure we keep track of the ServiceHost and close it when it has been removed from the RockBus configuration
                //  The DynamicServiceHostFactory will register any activated ServiceHosts with this DynamicServiceHostManager
                var activatedReceivePorts = DynamicServiceHelper.UpdateActivations(activeReceivePorts);

                foreach (var activatedReceivePort in activatedReceivePorts)
                {
                    using (var dynamicServiceHost = new DynamicServiceHost(this.Logger, activatedReceivePort))
                    {
                        if (dynamicServiceHost.ListenUri.Scheme == "net.msmq")
                        {
                            // Ensure the service is available when it concerns net.msmq, because msmq activation can take up to 10 minutes
                            //  http://msdn.microsoft.com/en-us/library/ms789042.aspx
                            //  http://msdn.microsoft.com/en-us/library/ms752246(v=vs.110).aspx
                            ServiceHostingEnvironment.EnsureServiceAvailable(activatedReceivePort.ReceiveEndpointInfo.Address);
                        }
                    }
                }

                var taskList = new List<Task>();

                // Go over all ServiceHosts that have not been marked, and therefore are to be closed and destroyed
                int removed = 0, closing = 0;
                foreach (var kvp in this.ServiceHostDictionary.ToArray())
                {
                    if (!kvp.Value.Item1)
                    {
                        ServiceHostEntry entryToRemove = null;
                        if (this.ServiceHostDictionary.TryRemove(kvp.Key, out entryToRemove))
                        {
                            ++removed;
                            // Asynchronously close the ServiceHost
                            //  Only if the WeakReference is still valid
                            ServiceHostBase serviceHost;
                            if (entryToRemove.Item3.TryGetTarget(out serviceHost))
                            {
                                ++closing;
                                var task = Task.Factory.FromAsync(this.BeginClose(serviceHost, 60000), this.EndClose);
                                taskList.Add(task);
                            }
                        }
                    }
                }

                this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "ServiceHosts Removed: {0}; Closing: {1}", removed, closing);

                // Wait for all service hosts to have been closed
                if (taskList.Count > 0)
                {
                    await Task.Factory.ContinueWhenAll(taskList.ToArray(), (ta) => { });
                    Task.WaitAll(taskList.ToArray());
                }
            }
            catch (Exception exc)
            {
                this.Logger.Log(MethodBase.GetCurrentMethod(), exc);
            }
            finally
            {
                this._guard = 0;
            }
        }

        public void RegisterDynamicServiceHost(ReceivePortInfo receivePortInfo, ServiceHostBase serviceHost)
        {
            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "Registering ReceivePort ServiceHosts: {0}", receivePortInfo.ToString());

            var key = new ServiceHostKey(receivePortInfo.PublisherInfo.Id, receivePortInfo.Id);
            var entry = new MutableTuple<bool, PublisherReceivePortInfo, WeakReference<ServiceHostBase>>(true, null, new WeakReference<ServiceHostBase>(serviceHost, false));
            this.ServiceHostDictionary.AddOrUpdate(key, entry, (k1,e1) => { return entry; });

            var dynamicServiceHost = serviceHost as IReceivePortServiceHost;
            if (dynamicServiceHost != null)
            {
                entry.Item2 = dynamicServiceHost.PublisherReceivePortInfo;
            }
        }

        public void RegisterStaticServiceHost(ServiceHostBase serviceHost)
        {
            // Intentionally left blank
        }

        private Task OpenServiceHostAsync(ServiceHostBase serviceHost)
        {
            return Task.Factory.FromAsync(this.BeginOpen(serviceHost, this.Configuration.ServiceHostOpenTimeoutMs), this.EndOpen);
        }

        private IAsyncResult BeginOpen(ServiceHostBase serviceHost, int timeoutMs)
        {
            return serviceHost.BeginOpen(TimeSpan.FromMilliseconds(timeoutMs), this.EndOpen, serviceHost);
        }

        private void EndOpen(IAsyncResult asyncResult)
        {
            var sh = asyncResult.AsyncState as ServiceHostBase;
        }

        private IAsyncResult BeginClose(ServiceHostBase serviceHost, int timeoutMs)
        {
            return serviceHost.BeginClose(TimeSpan.FromMilliseconds(timeoutMs), this.EndClose, serviceHost);
        }

        private void EndClose(IAsyncResult asyncResult)
        {
            var sh = asyncResult.AsyncState as ServiceHostBase;
        }

        private void SubscriptionConfigurationManager_OnSubscriptionConfigurationChanged(object sender, SubscriptionConfigurationChangedEventArgs e)
        {
            this.UpdateServiceHosts(e.PublisherConfiguration);
        }
    }
}